/************************************************************************
* AIDE - Automata Identification Engine
* Copyright (C) 2012-2013 Ali Khalili (khalili_DOT_ir_@SIGN_gmail_DOT_com)
* **********************************************************************
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License (LGPL) published 
* by the Free Software Foundation; either version 2.1 of the License, or (at your
* option) any later version. This program is distributed without any
* warranty. See the GNU Lesser General Public License for more details.
* ***********************************************************************
* File         :     AIDE\Filters\IAs\Filter_IA_Cached.cs
* Description  :     Providing the learning purpose for IAs
* Change log   :     12 March 2015: fix a bug, improve performance
************************************************************************/
using System;
using SMLib.Automata.IA;
using AIDE.Utils;
using SMLib.Automata.Base;
using System.Collections.Generic;
using System.IO;

namespace AIDE.Filters.IA
{
	/// <summary>
	/// Cache Filter for interface automata
	/// </summary>
	/// <exception cref='Exception'>
	/// Represents errors that occur during application execution.
	/// </exception>
	/// <exception cref='NotEnabledActionException'>
	/// Is thrown when the not enabled action exception.
	/// </exception>
	public class Filter_IA_Cached: IBlackBox_IA
	{
		/// <summary>
		/// State type
		/// </summary>
		enum stateType	{ACTIVE, PASSIVE, UNKNOWN};

		IBlackBox_IA sul;
		Tree<stateType,PairOfSymbols> cache;
		Tree<stateType,PairOfSymbols>.TreeNode root, currentNode;
		bool foundTillhere, isSynch;
		List<Step> executedTrace;
		int numberOfSULQueries, totalNumberOfQueries;
		StringSymbol OK, NOK, Delta;
		bool justReset;

		/// <summary>
		/// Initializes a new instance of the <see cref="AIDE.Filters.IA.Filter_IA_Cached"/> class.
		/// </summary>
		/// <param name='sul'>
		/// The system under learning
		/// </param>
		public Filter_IA_Cached (IBlackBox_IA sul)
		{						
			OK = new StringSymbol ("OK");
			NOK = new StringSymbol ("NOK");
			Delta = new StringSymbol ("Delta");			
			this.sul = sul;			
			justReset = true;
			cache = new Tree<stateType,PairOfSymbols> ();
			cache.Root.NodeObject = stateType.UNKNOWN;
			root = cache.Root;
			currentNode = root;
			foundTillhere = true;
			executedTrace = new List<Step> ();
		}



		/// <summary>
		/// Gets the number of saved queries.
		/// </summary>
		/// <value>
		/// The number of saved queries.
		/// </value>
		public int NumberOfCacheHit {
			get {
				return totalNumberOfQueries - numberOfSULQueries;
			}
		}

		/// <summary>
		/// Gets the total number of queries.
		/// </summary>
		/// <value>
		/// The total number of queries.
		/// </value>
		public int TotalNumberOfQueries {
			get {
				return totalNumberOfQueries;
			}
		}

		#region -=[ IMPORT and EXPORT]=-
		/* this works only if inputs and outputs are StringSymbol! */
		/* for the moment, we import/export pair symbols instead of traces! */
		System.Text.StringBuilder strB;
		StreamWriter useFilePathW;
		bool useFile;

		/// <summary>
		/// Actively uses a file
		/// </summary>
		/// <param name="path">Path of the file</param>
		public void UseFile (String path)
		{
			useFile = true;
			if (System.IO.File.Exists (path)) {
				ImportTraces (path);
				useFilePathW = File.AppendText (path); 
			} else {
				//useFilePathW = File.AppendText (path); 
				//System.IO.File.WriteAllText (path, "");
				useFilePathW = File.AppendText (path);
			}
		}

		void CommitTraceInFile (Sequence sq)
		{
			if (sq.Length == 0)
				return;
			useFilePathW.WriteLine (sq.ToString ());
			useFilePathW.Flush ();
		}

		public void ExportTraces (string filePath)
		{
			strB = new System.Text.StringBuilder ();
			Sequence sq = new Sequence ();
			ExportTree (root, sq);
			System.IO.File.WriteAllText (filePath, strB.ToString ());
		}

		void ExportTree (Tree<stateType,PairOfSymbols>.TreeNode node, Sequence sq)
		{
			if (node.NumberOfChilds == 0)
				strB.AppendLine (sq.ToString ());
			else {
				for (int i=0; i<node.NumberOfChilds; i++) {
					int l = sq.Length;
					sq.AddPair (node.Edges [i]);
					ExportTree (node.Childs [i], sq);
					sq.RemovePairAt (l);
				}				
			}
		}

		public void ImportTraces (string filePath)
		{
			System.IO.StreamReader sr = new System.IO.StreamReader (filePath);
			string line;
			
			while ((line=sr.ReadLine())!=null) {
				Sequence sq = Sequence.Parse (line);
				AddTrace (sq);
			}
		}

		/// <summary>
		/// Adds one trace to the cache
		/// </summary>
		/// <returns>
		/// The true if the trace was added, false if it was repeated
		/// </returns>
		/// <param name='sq'>
		/// The input sequence
		/// </param>
		/// <exception cref='Exception'>
		/// Represents errors that occur if the trace is not consistent with the current cache
		/// </exception>
		public bool AddTrace (Sequence sq)
		{
			if (!justReset)
				throw new Exception ("Cannot a trace add before reset!");
			
			bool tmpFoundTillhere = true;
			Tree<stateType,PairOfSymbols>.TreeNode tmpCurrentNode = root;
			for (int i=0; i<sq.Length; i++) {
				
				if (tmpFoundTillhere) {
					bool found = false;
					foreach (PairOfSymbols n in tmpCurrentNode.Edges) {
						if (n.Input.Equals (sq [i].Input)) {
							tmpCurrentNode = tmpCurrentNode.ChildByEdge (n);
							found = true;
                        
							if (!n.Output.Equals (sq [i].Output))
								throw new Exception ("Inconsistency in execution traces!");
							else
								break;
						}
					}
					if (found)
						continue;
					tmpFoundTillhere = false;
					i--;
				} else {
					if (sq [i].Input.Equals (Delta))
						tmpCurrentNode.NodeObject = stateType.ACTIVE;
					else
						tmpCurrentNode.NodeObject = stateType.PASSIVE;
					tmpCurrentNode = tmpCurrentNode.AddChild (stateType.UNKNOWN, sq [i]);
				}				
			}
			return tmpFoundTillhere;
		}
		#endregion


		#region IBlackBox_IA implementation
		public void Reset ()
		{

			if (!foundTillhere && useFile) {
				Sequence sq = new Sequence ();
				for (int i=0; i<executedTrace.Count; i++) {
					//sq.AddPair (executedTrace [i], executedTraceOutput [i]);
					//???
				}
				CommitTraceInFile (sq);
			}

			totalNumberOfQueries++;
			currentNode = root;
			foundTillhere = true;
			executedTrace.Clear ();
			justReset = true;
			isSynch = false;
		}

		bool CurrentStateIsActive ()
		{
			if (currentNode.NodeObject == stateType.ACTIVE)
				return true;
			else if (currentNode.NodeObject == stateType.PASSIVE)
				return false;
			else {
				if (!isSynch)
					SyncSUL ();

				if (sul.EventAvailable ()) {
					//TODO read also the event!
					currentNode.NodeObject = stateType.ACTIVE;
					return true;
				} else {
					currentNode.NodeObject = stateType.PASSIVE;
					return false;
				}
			}
		}

		/// <summary>
		/// Reach the SUL till the current state
		/// </summary>
		/// <exception cref='Exception'>
		/// Represents errors that occur during application execution.
		/// </exception>
		void SyncSUL ()
		{
			numberOfSULQueries++;
			sul.Reset ();
			//Console.Write ("cache missed");
			isSynch = true;

			ISymbol output;	
			//* experiment from the begining till here
			for (int i = 0; i < executedTrace.Count; i++) {
				if (executedTrace [i].IsInput) {
					sul.Perform (executedTrace [i].Symbol);
				} else {
					//* wait for the event!
					if (!sul.EventAvailable ()) {
						throw new Exception ("System is not consistent!");
					}
					output = sul.Event ();
					if (!output.Equals (executedTrace [i].Symbol))
						throw new Exception ("System is not consistent!");
				}
			}
		}

		public void Perform (SMLib.Automata.Base.ISymbol input)
		{
			//Console.Write ("[" + input + "]");
			justReset = false;
			ISymbol output;
			if (foundTillhere) {
				//* first check whather the current state is active or no
				if (CurrentStateIsActive ()) {
					throw new NotEnabledActionException ("");
				}
				
				foreach (PairOfSymbols n in currentNode.Edges) {
					if (n.Input.Equals (input)) {
						currentNode = currentNode.ChildByEdge (n);                        
						//* found!
						if (n.Output.Equals (NOK)) {
							throw new NotEnabledActionException ("");
						} else {
							isSynch = false;
							executedTrace.Add (new Step (input, true));
							return;
						}
					}
				}

				//* new path!
				foundTillhere = false;				
				if (!isSynch) {
					SyncSUL ();
				} 

				//* or maybe the action is not enabled?
				try {						
					sul.Perform (input);
					executedTrace.Add (new Step (input, true));
					output = OK;	
				} catch (NotEnabledActionException) {
					output = NOK;
				}
				currentNode = currentNode.AddChild (stateType.UNKNOWN, new PairOfSymbols (input, output));
				if (output.Equals (NOK))
					throw new NotEnabledActionException ("");
				return;
			} else {	
				//* first check whather the current state is active or no
				if (CurrentStateIsActive ())
					throw new NotEnabledActionException ("");
				//* or maybe the action is not enabled?
				try {
					sul.Perform (input);
					executedTrace.Add (new Step (input, true));
					output = OK;	
				} catch (NotEnabledActionException) {
					output = NOK;
				}
				currentNode = currentNode.AddChild (stateType.UNKNOWN, new PairOfSymbols (input, output));
			}
		}

		public bool EventAvailable ()
		{
			justReset = false;
			return CurrentStateIsActive ();
		}

		public SMLib.Automata.Base.ISymbol Event ()
		{
			justReset = false;
			ISymbol output;
			if (foundTillhere) {
				//* if there is no output here!
				if (!CurrentStateIsActive ())
					return null;
				
				if (currentNode.NumberOfChilds == 0) {
					//* we know that "CurrentStateIsActive" would reach!
					if (!isSynch) {
						SyncSUL ();
						if (!sul.EventAvailable ()) {
							throw new Exception ("System is not consistent!");
						}
					}
					output = sul.Event ();

					//* add next node
					currentNode = currentNode.AddChild (stateType.UNKNOWN, new PairOfSymbols (Delta, output));
				} else {
					output = currentNode.Edges [0].Output;
					if (!currentNode.Edges [0].Input.Equals (Delta))
						return null;

					currentNode = currentNode.Childs [0];
					isSynch = false;
				}
			} else {
				if (!CurrentStateIsActive ())
					return null;
				
				output = sul.Event ();
				currentNode = currentNode.AddChild (stateType.UNKNOWN, new PairOfSymbols (Delta, output));
			}

			executedTrace.Add (new Step (output, false));
			return output;
		}

		public SMLib.Automata.Base.Alphabet InputAlphabet {
			get {
				return sul.InputAlphabet;
			}
		}
		#endregion
	}
}

